package com.seelyn.rhea.mvc.internal.matcher;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.AnnotatedElement;
import java.util.*;

import static com.seelyn.rhea.mvc.common.utils.CheckConditionUtils.checkArgument;
import static com.seelyn.rhea.mvc.common.utils.CheckConditionUtils.checkNotNull;

/**
 * 匹配工具
 * @author linfeng
 * @since 2016/1/20
 */
public final class RheaMatcher {

    private RheaMatcher() {

    }

    private static final Matcher<Object> ANY = new Any();

    public static Matcher<Object> any() {
        return ANY;
    }

    public static Matcher<AnnotatedElement> annotatedWith(
            final Class<? extends Annotation> annotationType) {
        return new AnnotatedWithType(annotationType);
    }

    public static Matcher<AnnotatedElement> annotatedWith(
            final Annotation annotation) {
        return new AnnotatedWith(annotation);
    }

    private static void checkForRuntimeRetention(
            Class<? extends Annotation> annotationType) {
        Retention retention = annotationType.getAnnotation(Retention.class);
        checkArgument(retention != null && retention.value() == RetentionPolicy.RUNTIME,
                "Annotation %s is missing RUNTIME retention", annotationType.getSimpleName());
    }

    private static class Any extends AbstractMatcher<Object> implements Serializable {

        private static final long serialVersionUID = -3628025744595380533L;

        public boolean matching(Object o) {
            return true;
        }

        @Override
        public String toString() {
            return "any()";
        }

        public Object readResolve() {
            return any();
        }

    }

    private static class AnnotatedWithType extends AbstractMatcher<AnnotatedElement>
            implements Serializable {
        private static final long serialVersionUID = 1518543832397226966L;
        private final Class<? extends Annotation> annotationType;

        public AnnotatedWithType(Class<? extends Annotation> annotationType) {
            this.annotationType = checkNotNull(annotationType, "annotation type");
            checkForRuntimeRetention(annotationType);
        }

        public boolean matching(AnnotatedElement element) {
            return element.isAnnotationPresent(annotationType);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            AnnotatedWithType that = (AnnotatedWithType) o;
            return Objects.equals(annotationType, that.annotationType);
        }

        @Override
        public int hashCode() {
            return Objects.hash(annotationType);
        }

        @Override
        public String toString() {
            return "annotatedWith(" + annotationType.getSimpleName() + ".class)";
        }

    }

    private static class AnnotatedWith extends AbstractMatcher<AnnotatedElement>
            implements Serializable {
        private static final long serialVersionUID = 8135733627143956348L;
        private final transient Annotation annotation;

        public AnnotatedWith(Annotation annotation) {
            this.annotation = checkNotNull(annotation, "annotation");
            checkForRuntimeRetention(annotation.annotationType());
        }

        public boolean matching(AnnotatedElement element) {
            Annotation fromElement = element.getAnnotation(annotation.annotationType());
            return fromElement != null && annotation.equals(fromElement);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            AnnotatedWith that = (AnnotatedWith) o;
            return Objects.equals(annotation, that.annotation);
        }

        @Override
        public int hashCode() {
            return Objects.hash(annotation);
        }

        @Override
        public String toString() {
            return "annotatedWith(" + annotation + ")";
        }

    }

    public static UriMatcher<String> path(
            final String path) {
        return new PathMatcher(path);
    }

    private static class PathMatcher implements UriMatcher<String>, Serializable {

        private static final String PATH_SEPARATOR = "/";
        private static final long serialVersionUID = 6035939315236968521L;
        private List<UriMatcher<String>> match;

        public PathMatcher(String path) {
            this.match = toMatchChain(path);
        }

        private static List<UriMatcher<String>> toMatchChain(String path) {
            String[] pieces = path.split(PATH_SEPARATOR);

            List<UriMatcher<String>> matcherList = new ArrayList<UriMatcher<String>>();
            for (String piece : pieces) {
                if (piece.contains("*")) {
                    matcherList.add(new IgnorePathMatcher(piece));
                } else {
                    matcherList.add((piece.startsWith(":")) ? new ComplexPathMatcher(piece) : new SimplePathMatcher(piece));
                }
            }
            return Collections.unmodifiableList(matcherList);
        }

        @Override
        public boolean matching(String incoming) {
            Map<String, String> resultMap = getPathParams(incoming);
            return resultMap != null;
        }

        @Override
        public Map<String, String> getPathParams(String incoming) {
            String[] pieces = incoming.split(PATH_SEPARATOR);
            if (match.size() > pieces.length)
                return null;
            int i = 0;
            Map<String, String> values = new HashMap<String, String>();
            for (UriMatcher<String> pathMatcher : match) {
                if (i == pieces.length) {
                    return pathMatcher.matching("") ? values : null;
                }
                String piece = pieces[i];
                if (!pathMatcher.matching(piece)) {
                    return null;
                }
                values.putAll(pathMatcher.getPathParams(piece));
                i++;
            }
            return values;
        }
    }

    private static class ComplexPathMatcher implements UriMatcher<String>,Serializable {
        private static final long serialVersionUID = 2876648718376776443L;
        private final String key;
        private String value;

        public ComplexPathMatcher(String piece) {
            this.key = piece.substring(1);
        }

        @Override
        public boolean matching(String incoming) {
            this.value = incoming;
            return true;
        }

        @Override
        public Map<String, String> getPathParams(String incoming) {
            Map<String, String> oneMap = new HashMap<>(1);
            oneMap.put(key, value);
            return oneMap;
        }
    }

    private static class SimplePathMatcher implements UriMatcher<String>,Serializable {
        private static final long serialVersionUID = 432535834382631458L;
        private final String path;

        SimplePathMatcher(String path) {
            this.path = path;
        }

        @Override
        public boolean matching(String incoming) {
            return path.equals(incoming);
        }

        @Override
        public Map<String, String> getPathParams(String incoming) {
            return Collections.emptyMap();
        }
    }

    private static class IgnorePathMatcher implements UriMatcher<String>, Serializable {

        private static final long serialVersionUID = -8710054359931538279L;
        private String path;

        public IgnorePathMatcher(String path) {
            this.path = path;
        }

        @Override
        public Map<String, String> getPathParams(String incoming) {
            return Collections.emptyMap();
        }

        @Override
        public boolean matching(String s) {
            return true;
        }
    }

}
